Why Asynchronous Programming?
The Problem
Mobile apps need to perform operations that take time (network requests, file I/O, database queries). If these operations block the UI thread, the app becomes unresponsive and freezes.
The Solution
Asynchronous programming allows time-consuming operations to run in the background while the UI remains responsive. Flutter uses Dart's async/await syntax and Future/Stream APIs for this.
Key principle: Never block the UI thread. All I/O operations should be asynchronous.
Futures and async/await
What is a Future?
A Future represents a value that will be available at some point in the future. It's used for single asynchronous operations that return one value.
Basic Future Example
Future fetchData() async {
// Simulate network delay
await Future.delayed(Duration(seconds: 2));
return 'Data loaded';
}
// Usage with async/await
void loadData() async {
final data = await fetchData();
print(data); // Prints: Data loaded
}
async/await Syntax
- Mark functions that use
awaitwithasync awaitpauses execution until the Future completes- Async functions return a
Futureautomatically - Use
awaitto get the actual value from a Future
Multiple Async Operations
// Sequential (one after another)
Future loadSequential() async {
final user = await fetchUser();
final posts = await fetchPosts(user.id);
print('Loaded ${posts.length} posts');
}
// Parallel (at the same time)
Future loadParallel() async {
final results = await Future.wait([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
// All complete at the same time
}
Error Handling in Async Code
try-catch with async/await
Use try-catch blocks to handle errors in async functions:
Error Handling Example
Future loadDataSafely() async {
try {
final data = await fetchData();
print('Success: $data');
} on TimeoutException {
print('Request timed out');
} on HttpException catch (e) {
print('HTTP error: $e');
} catch (e) {
print('Unexpected error: $e');
}
}
Future Error Handling
You can also use then and catchError with Futures:
then/catchError Pattern
fetchData()
.then((data) {
print('Success: $data');
})
.catchError((error) {
print('Error: $error');
});
Streams for Continuous Data
What is a Stream?
A Stream provides a sequence of asynchronous events over time. Use streams for continuous data flows like user input, network responses, or file reading.
Basic Stream Example
Stream countStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // Emit value
}
}
// Listening to a stream
void listenToStream() {
countStream().listen(
(value) => print('Received: $value'),
onError: (error) => print('Error: $error'),
onDone: () => print('Stream completed'),
);
}
StreamBuilder Widget
Flutter's StreamBuilder widget rebuilds UI when stream emits new data:
StreamBuilder Example
StreamBuilder(
stream: countStream(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
return Text('Count: ${snapshot.data}');
},
)
Common Stream Operations
Stream Transformations
map()— Transform each eventwhere()— Filter eventstake()— Take first N eventsskip()— Skip first N eventsdistinct()— Remove duplicatesdebounceTime()— Wait for pause in events
Stream Transformations Example
// Transform stream
final numbers = Stream.fromIterable([1, 2, 3, 4, 5]);
final doubled = numbers.map((n) => n * 2);
final evens = numbers.where((n) => n % 2 == 0);
// Combine streams
final stream1 = Stream.value(1);
final stream2 = Stream.value(2);
final combined = StreamZip([stream1, stream2]);
Isolates for Heavy Computation
What are Isolates?
Isolates are separate execution threads that run in parallel. They don't share memory, so they're perfect for CPU-intensive tasks that would block the UI thread.
When to Use Isolates
- Heavy computations (image processing, large data parsing)
- Complex calculations that take significant time
- JSON parsing of very large files
- Any CPU-bound work that could freeze the UI
Using compute() Function
// Heavy computation function (must be top-level or static)
int calculateSum(List numbers) {
int sum = 0;
for (int num in numbers) {
sum += num * num; // Simulate heavy work
}
return sum;
}
// Use compute() to run in isolate
Future processData() async {
final numbers = List.generate(1000000, (i) => i);
final result = await compute(calculateSum, numbers);
print('Sum: $result'); // UI remains responsive
}
compute() Requirements
- The function must be top-level or static
- Parameters must be serializable (primitive types, lists, maps)
- Return value must be serializable
- No access to closures or instance variables
FutureBuilder and Async Operations in UI
FutureBuilder Widget
Use FutureBuilder to build UI based on Future completion:
FutureBuilder Example
FutureBuilder>(
future: fetchUsers(),
builder: (context, snapshot) {
// Check connection state
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
// Check for errors
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
// Check if data exists
if (!snapshot.hasData) {
return Text('No data');
}
// Build UI with data
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(title: Text(users[index].name));
},
);
},
)
ConnectionState Values
ConnectionState.none— No connection yetConnectionState.waiting— Waiting for dataConnectionState.active— Stream is active (for StreamBuilder)ConnectionState.done— Operation completed
Best Practices
Async Programming Best Practices
- Always handle errors in async operations
- Use
async/awaitinstead ofthen/catchErrorfor readability - Don't forget to check
mountedbefore setState in async callbacks - Use
Future.timeout()to prevent indefinite waiting - Cancel streams and futures when widgets are disposed
- Use
compute()for CPU-intensive tasks - Avoid creating unnecessary isolates for simple operations
- Prefer
StreamBuilderandFutureBuilderfor reactive UI updates
Exercises
1. Async Data Loading
Create a screen that fetches user data from an API (use a mock function). Use FutureBuilder to display loading, error, and success states. Add a retry button for error cases.
2. Stream-based Timer
Build a countdown timer using a Stream that emits values every second. Use StreamBuilder to display the remaining time. Add start, pause, and reset functionality.
3. Heavy Computation with Isolates
Create a function that calculates the sum of squares for a large list (1 million numbers). Use compute() to run it in an isolate. Show a loading indicator while computing and display the result when done. Compare the UI responsiveness with and without isolates.